import { isEmpty } from 'lodash';
import * as invariant from 'invariant';
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { Store } from 'redux';
import hoistNonReactStatics from './hoistNonReactStatics';

import {
    DAEMON,
    ONCE_TILL_UNMOUNT,
    RESTART_ON_REMOUNT,
} from '../constants/Saga';

const checkKey = (key: string) => invariant(
    !isEmpty(key),
    '(app/utils...) injectSaga: Expected `key` to be a non empty string'
);

export const injectSagaFactory = (store: Store<any>) => {
    return function injectSaga(key: string, descriptor: any = {}, args: any) {

        const newDescriptor = { ...descriptor, mode: descriptor.mode || RESTART_ON_REMOUNT };
        const { saga, mode } = newDescriptor;

        checkKey(key);

        let hasSaga = Reflect.has(store.injectedSagas, key);

        if (process.env.NODE_ENV !== 'production') {
            const oldDescriptor = store.injectedSagas[key];
            // enable hot reloading of daemon and once-till-unmount sagas
            if (hasSaga && oldDescriptor.saga !== saga) {
                oldDescriptor.task.cancel();
                hasSaga = false;
            }
        }

        if (!hasSaga || (hasSaga && mode !== DAEMON && mode !== ONCE_TILL_UNMOUNT)) {
            store.injectedSagas[key] = { ...newDescriptor, task: store.runSaga(saga, args) };
        }
    };
};

export const ejectSagaFactory = (store: Store<any>) => {
    return function ejectSaga(key: string) {

        checkKey(key);

        if (Reflect.has(store.injectedSagas, key)) {
            const descriptor = store.injectedSagas[key];
            if (descriptor.task) {
                descriptor.task.cancel();
                // Clean up in production; in development we need `descriptor.saga` for hot reloading
                if (process.env.NODE_ENV === 'production') {
                    // Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
                    store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
                }
            }
        }
    };
};

export const getInjectors = (store: any) => {
    return {
        injectSaga: injectSagaFactory(store),
        ejectSaga: ejectSagaFactory(store)
    };
};

/**
 * Dynamically injects a saga, passes component's props as saga arguments
 *
 * @param {string} key A key of the saga
 * @param {function} saga A root saga that will be injected
 * @param {string} [mode] By default (constants.RESTART_ON_REMOUNT) the saga will be started on component mount and
 * cancelled with `task.cancel()` on component un-mount for improved performance. Another two options:
 *   - constants.DAEMON—starts the saga on component mount and never cancels it or starts again,
 *   - constants.ONCE_TILL_UNMOUNT—behaves like 'RESTART_ON_REMOUNT' but never runs it again.
 *
 */
export const withSaga =
    ({ key, saga, mode }:
        {key: string, saga: any, mode?: any}) => (WrappedComponent: React.ComponentClass) => {
    class InjectSaga extends React.Component {
      static WrappedComponent = WrappedComponent;
      static contextTypes = {
        store: PropTypes.object.isRequired
      };
      static displayName = `withSaga(${(WrappedComponent.displayName || WrappedComponent.name || 'Component')})`;

      injectors = getInjectors(this.context.store);

      componentWillMount() {
        const { injectSaga } = this.injectors;

        injectSaga(key, { saga, mode }, this.props);
      }

      componentWillUnmount() {
        const { ejectSaga } = this.injectors;

        ejectSaga(key);
      }

      render() {
        return <WrappedComponent {...this.props} />;
      }
    }

    return hoistNonReactStatics(InjectSaga, WrappedComponent, null);
  };
